package com.hulk.dryad.manage.framework.idempotent.aop;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.hulk.dryad.common.annotation.Idempotent;
import com.hulk.dryad.common.constant.enums.BEC;
import com.hulk.dryad.common.util.ClassUtils;
import com.hulk.dryad.common.util.JacksonUtil;
import com.hulk.dryad.manage.framework.idempotent.exception.IdempotentException;
import com.hulk.dryad.manage.framework.idempotent.expression.IdempotentKeyResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.util.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
 * The Idempotent Aspect
 *
 * @author hulk
 */
@Slf4j
@Aspect
public class IdempotentAspect implements Ordered{


	private ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal();

	private static final String KEY = "KEY";
	private static final String DELKEY = "DELKEY";
	private static final String IDEMPOTENT_NAME_PREFIX = "IDEMPOTENT";
	private static final String IDEMPOTENT_NAME_SEPARATOR = "$";

	@Resource
	private IdempotentKeyResolver idempotentKeyResolver;
	@Resource
	private RedisTemplate redisTemplate;


	@Pointcut("@annotation(com.hulk.dryad.common.annotation.Idempotent)")
	public void pointCut() {
	}

	@Before("pointCut()")
	public void beforePointCut(JoinPoint joinPoint)  {
		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
				.getRequestAttributes();
		HttpServletRequest request = requestAttributes.getRequest();
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		if (!method.isAnnotationPresent(Idempotent.class)) {
			return;
		}
		Idempotent idempotent = method.getAnnotation(Idempotent.class);
		String url = request.getRequestURL().toString();
		String key = "";

		// 若没有配置 幂等 标识编号，则使用 url + 参数列表作为区分
		if (ObjectUtils.isEmpty(idempotent.keys())) {
			Object [] args =joinPoint.getArgs();
			Map<String, String[]> paramMap = request.getParameterMap();
			List<Object> beforeReqArgs = new ArrayList<>();
			paramMap.forEach((name, value) -> {
				beforeReqArgs.add(name+"="+StringUtils.arrayToCommaDelimitedString(value)+"&");
			});
			for (int i = 0; i < args.length; i++) {
				if (args[i] instanceof HttpServletResponse || args[i] instanceof HttpServletRequest) {
					continue;
				}
				// 读取方法参数
				MethodParameter methodParam = ClassUtils.getMethodParameter(method, i);
				// 一次请求只能有一个 request body
				RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
				// 如果是body的json则是对象
				if (requestBody != null) {
					beforeReqArgs.add(JacksonUtil.toJson(args[i]));
				}
				PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
				if (pathVariable != null) {
					beforeReqArgs.add(pathVariable.name()+"="+args[i]+"&");
				}
			}
			String	argString =beforeReqArgs.toString();
			key = url +":["+ argString+"]";
		}
		else {
			// 使用jstl 规则区分
			String argString  = idempotentKeyResolver.getKeyName(joinPoint,idempotent);
			key = url +":["+ argString+"]";
		}

		String expireTime = idempotent.expireTime();
		String info = idempotent.info();
		//TimeUnit timeUnit = idempotent.timeUnit();
		boolean delKey = idempotent.delKey();

		String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
		if(StrUtil.isNotBlank(authorization)){
			key = authorization+IDEMPOTENT_NAME_SEPARATOR+key;
		}
		if(key.length()>128){
			key = DigestUtil.md5Hex(key, StandardCharsets.UTF_8);
		}
		key = IDEMPOTENT_NAME_PREFIX+IDEMPOTENT_NAME_SEPARATOR+key;

		String value = LocalDateTime.now().toString().replace("T", " ");
		boolean v1;
		if (null != redisTemplate.opsForValue().get(key)) {
            // had stored
            throw new IdempotentException(BEC.E_2023.getErrCode(), info);
		}
		synchronized (this) {
			Duration duration = DurationStyle.detectAndParse(expireTime, ChronoUnit.MILLIS);
			v1 = redisTemplate.opsForValue().setIfAbsent(key, value, duration);
			if (!v1) {
				throw new IdempotentException(BEC.E_2023.getErrCode() , info);
			}
			else {
				log.debug("[idempotent]:has stored key=[{}],value=[{}],expireTime=[{}],now=[{}]", key, value, duration,LocalDateTime.now().toString());
			}
		}

		Map<String, Object> map = CollectionUtils.isEmpty(threadLocal.get()) ? new HashMap<>(4) : threadLocal.get();
		map.put(KEY, key);
		map.put(DELKEY, delKey);
		threadLocal.set(map);

	}

	@After("pointCut()")
	public void afterPointCut(JoinPoint joinPoint) {
		Map<String, Object> map = threadLocal.get();
		if (CollectionUtils.isEmpty(map)) {
			return;
		}
		//get key value
		String key =(String) map.get(KEY);
		if (redisTemplate.opsForValue().get(key) == null) {
			threadLocal.remove();
			return;
		}
		boolean delKey = (boolean) map.get(DELKEY);
		if (delKey) {
			redisTemplate.delete(key);
			log.info("[idempotent]:has removed key=[{}]", key);
		}
		threadLocal.remove();
	}

	@Override
	public int getOrder() {
		return Ordered.LOWEST_PRECEDENCE;
	}
}
